Explorez les alternatives TypeScript puissantes aux énumérations : assertions const et types union. Apprenez à utiliser chacun pour un code robuste et maintenable.
Au-delà des énumérations : assertions const de TypeScript vs. types union
Dans le monde de JavaScript typé statiquement avec TypeScript, les énumérations sont depuis longtemps une solution de choix pour représenter un ensemble fixe de constantes nommées. Elles offrent un moyen clair et lisible de définir un ensemble de valeurs associées. Cependant, à mesure que les projets croissent et évoluent, les développeurs recherchent souvent des alternatives plus flexibles et parfois plus performantes. Deux concurrents puissants qui émergent fréquemment sont les assertions const et les types union. Cet article se penche sur les nuances de l’utilisation de ces alternatives aux énumérations traditionnelles, en fournissant des exemples pratiques et en vous guidant sur le choix de l’une ou l’autre.
Comprendre les énumérations TypeScript traditionnelles
Avant d’explorer les alternatives, il est essentiel de bien comprendre le fonctionnement des énumérations TypeScript standard. Les énumérations vous permettent de définir un ensemble de constantes numériques ou de chaînes nommées. Elles peuvent être numériques (par défaut) ou basées sur des chaînes.
Énumérations numériques
Par défaut, les membres de l’énumération se voient attribuer des valeurs numériques à partir de 0.
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Output: 0
Vous pouvez également attribuer explicitement des valeurs numériques.
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Output: 200
Énumérations de chaînes
Les énumérations de chaînes sont souvent préférées pour leur expérience de débogage améliorée, car les noms des membres sont conservés dans le JavaScript compilé.
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Output: "BLUE"
La surcharge des énumérations
Bien que les énumérations soient pratiques, elles entraînent une légère surcharge. Une fois compilées en JavaScript, les énumérations TypeScript sont transformées en objets qui ont souvent des mappages inverses (par exemple, mapper la valeur numérique au nom de l’énumération). Cela peut être utile, mais contribue également à la taille du bundle et peut ne pas toujours être nécessaire.
Considérez cette simple énumération de chaînes :
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
En JavaScript, cela pourrait devenir quelque chose comme :
var Status;
(function (Status) {
Status["Pending"] = "PENDING";
Status["Processing"] = "PROCESSING";
Status["Completed"] = "COMPLETED";
})(Status || (Status = {}));
Pour les ensembles de constantes simples et en lecture seule, ce code généré peut sembler un peu excessif.
Alternative 1 : Assertions const
Les assertions const sont une fonctionnalité TypeScript puissante qui vous permet de dire au compilateur de déduire le type le plus spécifique possible pour une valeur. Lorsqu’elles sont utilisées avec des tableaux ou des objets destinés à représenter un ensemble fixe de valeurs, elles peuvent servir d’alternative légère aux énumérations.
Assertions const avec des tableaux
Vous pouvez créer un tableau de littéraux de chaînes, puis utiliser une assertion const pour rendre son type immuable et ses éléments de types littéraux.
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Error: Type '"FAILED"' is not assignable to type 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Processing status: ${status}`);
}
processStatus("COMPLETED");
Décomposons ce qui se passe ici :
as const : Cette assertion indique à TypeScript de traiter le tableau comme étant en lecture seule et de déduire les types littéraux les plus spécifiques pour ses éléments. Ainsi, au lieu destring[], le type devientreadonly ["PENDING", "PROCESSING", "COMPLETED"].typeof statusArray[number] : Il s’agit d’un type mappé. Il itère sur tous les index dustatusArrayet extrait leurs types littéraux. La signature d’indexnumberindique essentiellement « donnez-moi le type de n’importe quel élément de ce tableau ». Le résultat est un type union :"PENDING" | "PROCESSING" | "COMPLETED".
Cette approche offre une sécurité de type similaire aux énumérations de chaînes, mais génère un JavaScript minimal. Le statusArray lui-même reste un tableau de chaînes en JavaScript.
Assertions const avec des objets
Les assertions const sont encore plus puissantes lorsqu’elles sont appliquées à des objets. Vous pouvez définir un objet où les clés représentent vos constantes nommées et les valeurs sont les chaînes ou les nombres littéraux.
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Error: Type '"GUEST"' is not assignable to type 'UserRole'.
function displayRole(role: UserRole) {
console.log(`User role is: ${role}`);
}
displayRole(userRoles.Admin); // Valid
displayRole("EDITOR"); // Valid
Dans cet exemple d’objet :
as const : Cette assertion rend l’objet entier en lecture seule. Plus important encore, elle déduit les types littéraux pour toutes les valeurs de propriété (par exemple,"ADMIN"au lieu destring) et rend les propriétés elles-mêmes en lecture seule.keyof typeof userRoles : Cette expression génère une union des clés de l’objetuserRoles, qui est"Admin" | "Editor" | "Viewer".typeof userRoles[keyof typeof userRoles] : Il s’agit d’un type de recherche. Il prend l’union des clés et l’utilise pour rechercher les valeurs correspondantes dans le typeuserRoles. Cela se traduit par l’union des valeurs :"ADMIN" | "EDITOR" | "VIEWER", qui est notre type souhaité pour les rôles.
La sortie JavaScript pour userRoles sera un objet JavaScript simple :
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
C’est beaucoup plus léger qu’une énumération typique.
Quand utiliser les assertions const
- Constantes en lecture seule : Lorsque vous avez besoin d’un ensemble fixe de chaînes ou de nombres littéraux qui ne doivent pas changer au moment de l’exécution.
- Sortie JavaScript minimale : Si vous vous souciez de la taille du bundle et que vous souhaitez la représentation d’exécution la plus performante pour vos constantes.
- Structure de type objet : Lorsque vous préférez la lisibilité des paires clé-valeur, de la même manière que vous structureriez les données ou la configuration.
- Ensembles basés sur des chaînes : Particulièrement utile pour représenter des états, des types ou des catégories qui sont mieux identifiés par des chaînes descriptives.
Alternative 2 : Types union
Les types union vous permettent de déclarer qu’une variable peut contenir une valeur de l’un de plusieurs types. Lorsqu’ils sont combinés avec des types littéraux (littéraux de chaînes, de nombres, booléens), ils constituent un moyen puissant de définir un ensemble de valeurs autorisées sans avoir besoin d’une déclaration de constante explicite pour l’ensemble lui-même.
Types union avec des littéraux de chaînes
Vous pouvez définir directement une union de littéraux de chaînes.
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Error: Type '"BLUE"' is not assignable to type 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Changing light to: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Error
C’est le moyen le plus direct et souvent le plus concis de définir un ensemble de valeurs de chaînes autorisées.
Types union avec des littéraux numériques
De même, vous pouvez utiliser des littéraux numériques.
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Error: Type '201' is not assignable to type 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("Success!");
} else {
console.log(`Error code: ${code}`);
}
}
handleResponse(500);
Quand utiliser les types union
- Ensembles simples et directs : Lorsque l’ensemble de valeurs autorisées est petit, clair et ne nécessite pas de clés descriptives au-delà des valeurs elles-mêmes.
- Constantes implicites : Lorsque vous n’avez pas besoin de faire référence à une constante nommée pour l’ensemble lui-même, mais plutôt d’utiliser directement les valeurs littérales.
- Concision maximale : Pour les scénarios simples où la définition d’un objet ou d’un tableau dédié semble excessive.
- Paramètres/types de retour de fonction : Excellent pour définir l’ensemble exact d’entrées/sorties de chaînes ou de nombres acceptables pour les fonctions.
Comparaison des énumérations, des assertions const et des types union
Résumons les principales différences et cas d’utilisation :
Comportement d’exécution
- Énumérations : Générer des objets JavaScript, potentiellement avec des mappages inverses.
- Assertions const (tableaux/objets) : Générer des tableaux ou des objets JavaScript simples. Les informations de type sont effacées au moment de l’exécution, mais la structure de données reste.
- Types union (avec des littéraux) : Aucune représentation d’exécution pour l’union elle-même. Les valeurs ne sont que des littéraux. La vérification de type se fait uniquement au moment de la compilation.
Lisibilité et expressivité
- Énumérations : Lisibilité élevée, en particulier avec des noms descriptifs. Peut être plus verbeux.
- Assertions const (objets) : Bonne lisibilité grâce aux paires clé-valeur, imitant les configurations ou les paramètres.
- Assertions const (tableaux) : Moins lisible pour représenter des constantes nommées, plus pour une simple liste ordonnée de valeurs.
- Types union : Très concis. La lisibilité dépend de la clarté des valeurs littérales elles-mêmes.
Sécurité de type
- Les trois approches offrent une forte sécurité de type. Elles garantissent que seules les valeurs valides et prédéfinies peuvent être attribuées à des variables ou transmises à des fonctions.
Taille du bundle
- Énumérations : Généralement les plus grandes en raison des objets JavaScript générés.
- Assertions const : Plus petites que les énumérations, car elles produisent des structures de données simples.
- Types union : Les plus petits, car ils ne génèrent aucune structure de données d’exécution spécifique pour le type lui-même, s’appuyant uniquement sur des valeurs littérales.
Matrice des cas d’utilisation
Voici un guide rapide :
| Fonctionnalité | Énumération TypeScript | Assertion const (objet) | Assertion const (tableau) | Type union (littéraux) |
|---|---|---|---|---|
| Sortie d’exécution | Objet JS (avec mappage inverse) | Objet JS simple | Tableau JS simple | Aucun (uniquement les valeurs littérales) |
| Lisibilité (constantes nommées) | Élevée | Élevée | Moyenne | Faible (les valeurs sont des noms) |
| Taille du bundle | La plus grande | Moyenne | Moyenne | La plus petite |
| Flexibilité | Bonne | Bonne | Bonne | Excellente (pour les ensembles simples) |
| Utilisation courante | États, codes d’état, catégories | Configuration, définitions de rôles, indicateurs de fonctionnalité | Listes ordonnées de valeurs immuables | Paramètres de fonction, valeurs restreintes simples |
Exemples pratiques et bonnes pratiques
Exemple 1 : Représentation des codes d’état de l’API
Énumération :
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Assertion const (objet)Â :
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Type union :
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Recommandation : Pour ce scénario, un type union est souvent le plus concis et le plus efficace. Les valeurs littérales elles-mêmes sont suffisamment descriptives. Si vous deviez associer des métadonnées supplémentaires à chaque état (par exemple, un message convivial), un objet d’assertion const serait un meilleur choix.
Exemple 2 : Définition des rôles d’utilisateur
Énumération :
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... logic ...
}
Assertion const (objet)Â :
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Type union :
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Recommandation : Un objet d’assertion const établit un bon équilibre ici. Il fournit des paires clé-valeur claires (par exemple, userRolesObject.Admin) qui peuvent améliorer la lisibilité lors de la référence aux rôles, tout en étant performant. Un type union est également un concurrent très fort si des littéraux de chaînes directs sont suffisants.
Exemple 3 : Représentation des options de configuration
Imaginez un objet de configuration pour une application globale qui pourrait avoir différents thèmes.
Énumération :
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Assertion const (objet)Â :
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Type union :
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Recommandation : Pour les paramètres de configuration tels que les thèmes, l’objet d’assertion const est souvent idéal. Il définit clairement les options disponibles et leurs valeurs de chaînes correspondantes. Les clés (Light, Dark, System) sont descriptives et correspondent directement aux valeurs, ce qui rend le code de configuration très compréhensible.
Choisir le bon outil pour le travail
La décision entre les énumérations TypeScript, les assertions const et les types union n’est pas toujours noire ou blanche. Elle se résume souvent à un compromis entre les performances d’exécution, la taille du bundle et la lisibilité/l’expressivité du code.
- Optez pour les types union lorsque vous avez besoin d’un ensemble simple et contraint de littéraux de chaînes ou de nombres et qu’une concision maximale est souhaitée. Ils sont excellents pour les signatures de fonction et les restrictions de valeurs de base.
- Optez pour les assertions const (avec des objets) lorsque vous souhaitez un moyen plus structuré et lisible de définir des constantes nommées, similaires à une énumération, mais avec une surcharge d’exécution considérablement moindre. C’est idéal pour la configuration, les rôles ou tout ensemble où les clés ajoutent une signification importante.
- Optez pour les assertions const (avec des tableaux) lorsque vous avez simplement besoin d’une liste ordonnée immuable de valeurs, et que l’accès direct via l’index est plus important que les clés nommées.
- Considérez les énumérations TypeScript lorsque vous avez besoin de leurs fonctionnalités spécifiques, telles que le mappage inverse (bien que cela soit moins courant dans le développement moderne) ou si votre équipe a une forte préférence et que l’impact sur les performances est négligeable pour votre projet.
Dans de nombreux projets TypeScript modernes, vous constaterez une tendance vers les assertions const et les types union par rapport aux énumérations traditionnelles, en particulier pour les constantes basées sur des chaînes, en raison de leurs meilleures caractéristiques de performances et d’une sortie JavaScript souvent plus simple.
Considérations générales
Lors du développement d’applications pour un public mondial, des définitions de constantes cohérentes et prévisibles sont essentielles. Les choix dont nous avons parlé (énumérations, assertions const, types union) contribuent tous à cette cohérence en appliquant la sécurité de type dans différents environnements et paramètres régionaux de développeurs.
- Cohérence : Quelle que soit la méthode choisie, la clé est la cohérence au sein de votre projet. Si vous décidez d’utiliser des objets d’assertion const pour les rôles, respectez ce modèle dans tout le code.
- Internationalisation (i18n) : Lors de la définition d’étiquettes ou de messages qui seront internationalisés, utilisez ces structures de type sécurisé pour vous assurer que seules les clés ou les identificateurs valides sont utilisés. Les chaînes traduites réelles seront gérées séparément via les bibliothèques i18n. Par exemple, si vous avez un champ
statusqui peut être « PENDING », « PROCESSING », « COMPLETED », votre bibliothèque i18n mapperait ces identificateurs internes au texte d’affichage localisé. - Fuseaux horaires et devises : Bien que cela ne soit pas directement lié aux énumérations, n’oubliez pas que lorsque vous traitez des valeurs telles que les dates, les heures ou les devises, le système de type de TypeScript peut aider à appliquer une utilisation correcte, mais des bibliothèques externes sont généralement nécessaires pour une gestion globale précise. Par exemple, un type union
Currencypourrait être défini comme"USD" | "EUR" | "GBP", mais la logique de conversion réelle nécessite des outils spécialisés.
Conclusion
TypeScript fournit un ensemble riche d’outils pour la gestion des constantes. Bien que les énumérations nous aient bien servi, les assertions const et les types union offrent des alternatives convaincantes, souvent plus performantes. En comprenant leurs différences et en choisissant la bonne approche en fonction de vos besoins spécifiques, qu’il s’agisse de performances, de lisibilité ou de concision, vous pouvez écrire un code TypeScript plus robuste, plus maintenable et plus efficace qui s’adapte à l’échelle mondiale.
L’adoption de ces alternatives peut entraîner une réduction de la taille des bundles, des applications plus rapides et une expérience de développement plus prévisible pour votre équipe internationale.